#!/usr/bin/env python3

from bottle import route, run, template, Bottle, request, response, static_file, HTTPError, abort, redirect 
import dataset   # for database management
from hashlib import md5, sha512  # for authentication
import os        # for file paths
import time      # for Expires and Last-Modified
import magic     # for those pesky mimetypes!
import string
from argparse import ArgumentParser  # for the command line arguments
from uuid import uuid4              # for pseudorandom number generation
from random import SystemRandom     # for file IDs and captcha generation
from threading import Thread  # for file deletion

from sys import path, getsizeof
path.insert(1, 'lib/')

import safeurl
from bottle_custom import custom_static_file
from skimpyGimpy import skimpyAPI


ROOT = '/var/www/p/'
IS_DEV = False

HOST = '127.0.0.1'
PORT = 9090

ALLOWED_CHARS = string.ascii_letters + string.digits + '_.-'
CLEAN_INTERVAL = 300  # 5 minutes

if __name__ == '__main__':
    parser = ArgumentParser(description='Hydra Paste, the little thing of ours.')
    parser.add_argument('--dev', help='developer mode; path', action='store_true')
    parser.add_argument('--host', help='host to listen at; ip or host', nargs='?', const=HOST)
    parser.add_argument('--port', help='port to listen at; integer 1024-65535', nargs='?', const=PORT, type=int)
    args = parser.parse_args()

    if args.dev == True:
        IS_DEV = True
        ROOT = os.getcwd()
        print('\nRunning in developer mode at `{}`'.format(ROOT))

    if args.host:
        HOST = args.host

    if args.port:
        PORT = args.port


DIR_TEMPLATES = os.path.join(ROOT, 'static/')
DIR_FILES = os.path.join(ROOT, 'files/')
DATABASE = os.path.join(ROOT, 'db.sqlite')

ui = Bottle()
try:
    print('\nOpening database `{}`...'.format(DATABASE))
    db = dataset.connect('sqlite:///{}'.format(DATABASE))
except:
    print('Unable to open database. Exiting program...')
    quit()
    pass

SECRAND = SystemRandom()  # generator of randomness
CAPTCHA_SESSIONS = []  # captcha cookies



#######################################
#                                     #
#   DON'T TOUCH SHIT FROM DOWN HERE   #
#                                     #
#######################################

# Create the database tables if they don't exist
for table_type in ['files', 'users', 'captcha', 'session']:
    if table_type not in db.tables:
        db.create_table(table_type)


#######################################
# Login, sessions and shit            #
#######################################

def get_login_creds():
    """
    Get the username/password combination from a request

    Returns a tuple in the form of (username, password)
    """
    # TODO: Improve robustness of this code
    if request.method == 'POST':
        username = request.forms.getunicode('username')
        password = request.forms.getunicode('password')

        if not username:
            abort(401, 'You need to be logged in to do this')

        return (username, password)
    else:
        if 'Authorization' in request.headers:
            _method, user_pass_combo = request.headers['Authorization'].split()
            username, password = user_pass_combo.split(':')
            return (username, password)
        else:
            abort(401)


def check_login(usr, pwd, anon=True):
    """
    Check for password login, or anonymous login.

    `usr` - username for login to check
    `pwd` - password for login to check
    `anon` - whether or not to allow anonymous logins

    Returns: `True` if login is good, `False` if otherwise.
    """
    if usr == 'anonymous':  # Anonymous login
        return anon
    else:  # TODO: Check SQLite DB
        result = db['users'].find_one(username=usr)

        if result and pwd:  # login exists
            salt_hash_comb = result['p_hash']

            # cipher_num is for future migrations/logins
            # 6 = sha-512
            cipher_num, salt, login_hash = salt_hash_comb.split('$')

            calc_hash = sha512(bytes(salt + pwd, 'utf-8')).hexdigest()

            if calc_hash == login_hash:
                return True

        return False


def check_login_session():
    """
    Check that a user's session is good by looking at their cookies.

    Cookies are generated in start_login_session()
    Cookies are removed in end_login_session()
    """
    login_session = request.get_cookie('login_session')

    if login_session and ':' in login_session:
        username, session = [x for x in login_session.split(':', 1)]
        user_row = db['session'].find_one(user=username, session=session)

        if user_row:
            match_id = '{}:{}'.format(user_row['user'], user_row['session'])
            return match_id == login_session
        else:
            return False
    else:
        return False


def remove_session(session_id=None, username=None):
    """
    Search for a login session in the sqlite database and remove it
    if the user or session exists.

    For use with internal API only.

    `session_id` - Session ID that will be removed
    `username` - If `session_id` is not specified, `username` will get session removed
    """
    assert not (session_id and username), 'You may only pass one parameter at once'
    assert username or session_id, 'You must pass a parameter to this function'

    results = None

    if session_id:
        results = db['session'].find(session=session_id)
    elif username:
        results = db['session'].find(user=username)
    else:
        abort(500, 'Uh oh. Were you not logged in?')

    db.begin()
    for row in results:
        row_id = row['id']
        db['session'].delete(id=row_id)
        print('Deleted row {}'.format(row_id))
    db.commit()


def start_login_session(usr, remember=False):
    """
    Gives a user a cookie to start their login session

    `usr` - The user that will get a cookie
    `remember` - Whether to have the login session "remembered"
    """
    # Find session for user in database and remove it
    remove_session(username=usr)

    # Format of the session cookie is `user:session_id`
    session_id = sha512(bytes(uuid4().hex, 'utf-8')).hexdigest()
    session_cookie = '{}:{}'.format(usr, session_id)

    # Add session to database
    db.begin()
    db['session'].insert({'user': usr, 'session': session_id})
    db.commit()

    if remember:
        expire_time = time.time() + 14 * 24 * 3600  # Expire in 14 days
        response.set_cookie('login_session', session_cookie, expires=expire_time)
    else:
        response.set_cookie('login_session', session_cookie)


def _get_session_username():
    """
    Get the username of the current session in the request
    NOTE: This DOES NOT verify the user is logged in, and should
    be used for cosmetic purposes only.

    Returns: `username` of the user with a cookie (?)
    """
    login_session = request.get_cookie('login_session')

    if login_session and ':' in login_session:
        return login_session.split(':', 1)[0]
    else:
        return None


def check_captcha(cookie_id, captcha_answer):
    """
    Check a captcha answer and cookie ID against stored credentials

    `cookie_id` - Unique ID of a session
    `captcha_answer` - Answer returned by the user

    Returns: `True` if the answer matches the known solution
    """
    for auth_pair in CAPTCHA_SESSIONS:
        print(auth_pair)
        if auth_pair[0] == cookie_id and auth_pair[1] == captcha_answer:
            CAPTCHA_SESSIONS.remove(auth_pair)  # Remove correctly solved session, we don't need it anymore
            return True

    return False


#######################################
# General functions                   #
#######################################


def generate_etag(param_list):
    """
    Generate a weak ETag from a list of parameters.

    ETAG is generated from concatenating items in param_list,
    and hashing the result through an MD5 Hash.

    MD5 is used because it is fast. In this case, the ETag does
    not have to be secure, so using MD5 is not an issue.

    `param_list` - List of things to be hashed into an ETag

    Returns: ETag of `param_list`
    """
    combo = ''

    for param in param_list:
        combo += str(param)

    # 'W/' for weak ETags
    return 'W/' + md5(bytes(combo, 'utf-8')).hexdigest()


def get_random_key():
    """
    Get a file key that isn't already taken

    Returns: an unique key
    """
    key = SECRAND.randint(0, 66 ** 4)

    while True:
        if not db['files'].find_one(key=key):
            return key
        key = SECRAND.randint(0, 66 ** 4)  # O(1) operation


def sanitize(unsafe_input):
    """
    Check for unsafe input
    """
    for character in unsafe_input:
        if character not in ALLOWED_CHARS:
            return False
    return True


def user_pastes(username):
    """
    Return a dicitonary of a user's pastes

    username - the user to get pastes from
    """
    search_results = db['files'].find(user=username)
    data = {}

    for paste in search_results:
        key = paste['key']
        short_id = safeurl.num_encode(key)

        data[short_id] = api_file_info(short_id)

    return data


#######################################
# Files and shit                      #
#######################################

def get_file(fileid, mime_type, req=None, download=False, filename=None):
    """
    Get a file

    `fileid` - The unique ID of a file
    `mime_type` - Used to identify a file's type
    `headers` - If we want to specify special headers
    `req` - Internal request object
    `download` - Force download of a file
    `filename` - Original name of a file

    Returns: File
    """
    file_info = api_file_info(fileid, headers=True)
    if download:
        return custom_static_file(fileid, root=DIR_FILES, request=req, custom_headers=file_info, mimetype=mime_type, download=filename)
    else:
        return custom_static_file(fileid, root=DIR_FILES, request=req, custom_headers=file_info, mimetype=mime_type)


def increment_view_count(paste):
    """
    Increment the view counter of a paste by 1.

    `paste` - the short key of a paste
    """
    long_key = safeurl.num_decode(paste)

    row = db['files'].find_one(key=long_key)
    db.begin()
    db['files'].update({'key': row['key'], 'views': (row['views'] + 1)}, ['key'])
    db.commit()


#######################################
# API Stuff                           #
#######################################


@ui.post('/api/user')
@ui.post('/api/user/')
def api_user_pastes():
    """
    API for viewing a user's pastes.
    User must be logged in to see own pastes.

    Returns: Data if the user is logged in, otherwise error 401
    """
    username, password = get_login_creds()

    if check_login(username, password, anon=False):
        return user_pastes(username)
    elif username == 'anonymous':
        abort(401, 'You must have an account to do this')
    else:
        abort(401, 'You need to log in to do this.')


@ui.delete('/api/file/<fileid>')  # REST API
@ui.post('/api/file/rm/<fileid>')  # Pleb API
def api_file_delete(fileid):
    """
    Delete a file by its file id.

    Authorization is in the user-pass format.
    """
    user, password = get_login_creds()

    file_info = api_file_info(fileid)
    long_key = safeurl.num_decode(fileid)

    if check_login(user, password, anon=False) and file_info['user'].lower() == user.lower():
        long_key = safeurl.num_decode(fileid)

        db.begin()
        db['files'].delete(key=long_key)
        db.commit()
        try:
            os.remove(os.path.join(DIR_FILES, fileid))
        except:
            abort(500, 'Something bad happened. That file isn\'t here :(')
        print('[DBG] Deleted file: {}'.format(fileid))

        return 'File deleted'
    else:
        abort(401)


@ui.route('/api/file/<fileid>')
@ui.route('/api/file/get/<fileid>')  # TODO: Depreciate
def api_get_file(fileid):
    """
    Retrieve information about a file or download it.

    #: `GET`  - Downloads file `fileid`
    #: `HEAD` - Downloads information about a file from it's headers.
    """
    real_key = safeurl.num_decode(fileid)
    row = db['files'].find_one(key=real_key)  # At this point, `fileid` is safe
    if row:
        dupe_view = False  # Whether this view is a duplicate view

        # Check if referer is web UI
        if 'Referer' in request.headers:
            referer = request.headers['Referer']
            if '.' in referer:
                base_referer = referer.split('.', 1)
                if base_referer[1] == 'hydra.ws/p/{}'.format(fileid):
                    dupe_view = True

        if not dupe_view and request.method == 'GET':
            increment_view_count(fileid)

        mime_type = row['filetype']

        return get_file(fileid, mime_type, req=request)
    else:
        abort(404)


@ui.route('/api/file/info/<fileid>')
def api_file_info(fileid, headers=False):
    """
    Get information about a file

    `fileid` - Unique file ID
    `headers` - To send custom headers or not

    Returns: Information about `fileid`, error 404 otherwise
    """
    file_key = safeurl.num_decode(fileid)

    db_result = db['files'].find_one(key=file_key)

    if db_result and headers:
        # Create ETag for file
        last_modified = db_result['time']
        filename = db_result['filename']
        etag = generate_etag(('static-', filename, last_modified))

        return {'X-Paste-User': db_result['user'],
                'X-Paste-Expires': db_result['expires'],
                'X-Paste-Filename': filename,
                'X-Paste-Title': db_result['title'],
                'X-Paste-Views': db_result['views'],
                'Content-Type': db_result['filetype'],
                'Content-Length': db_result['filesize'],
                'Last-Modified': time.strftime('%a, %d %b %Y %H:%M:%S +0000', time.gmtime(last_modified)),
                'ETag': etag}
    elif db_result and not headers:
        return {'user': db_result['user'],
                'expires': int(db_result['expires']),
                'filename': db_result['filename'],
                'filetype': db_result['filetype'],
                'filesize': int(db_result['filesize']),
                'title': db_result['title'],
                'time': db_result['time'],
                'views': db_result['views'],
                'key': fileid}
    else:
        abort(404)


@ui.route('/api/upload')
def wrong_get_upload():
    abort(400, 'Use a POST, not a GET.')


@ui.post('/api/file/<fileid>')  # For file updates
@ui.post('/api/upload')  # For initial uploads
def api_upload(fileid=None, web_ui=False, username=None):
    """
    Function for uploading files to server.
    Allows option to modify existing file if uploaded from personal account.

    fileid - if non-blank, the short ID of the file to upload
    web_ui - whether this upload was via the web ui - assumes correct authentication
        username - webui username
    """
    password = None

    if not web_ui:
        username, password = get_login_creds()

    file_text = request.forms.getunicode('text', default=None)

    if username.strip() == '':
        abort(401, 'You must specify a login to use this command')
    if not web_ui and not check_login(username, password, anon=True):
        abort(401, 'Bad login')

    # File upload information
    upload_time = int(time.time())
    upload = request.files.get('upload')
    upload_size = None
    file_name = None
    if upload:
        upload_size = request.content_length  # For internal purposes, this
        # is a rough hack because bottle's FileUpload.content_length is broken
        file_name = upload.filename
    else:
        upload_size = getsizeof(bytes(file_text, 'utf-8'))

    # Size + 400 because of metadata included in request header
    if file_text and upload_size >= 10000000 + 400:
        abort(413, 'You may only upload text up to 10 MB large')
    elif not file_text and upload_size >= 150000000 + 400:
        abort(413, 'You may only upload files up to 150 MB large')

    expires = None

    try:
        expires = request.forms.get('expires')

        delta_time = None

        if '+' in expires:  # If delta time
            delta_time = int(expires.strip()[1:])

            expires = int(time.time()) + delta_time

        else:  # If manual time time
            if expires is None:  # not specified, give default time
                expires = int(time.time()) + 518400  # 518400 sec = 6 days
            elif int(expires) == 0:  # Never expires
                expires = 0
            else:  # Give custom time
                expires = int(expires)
    except:
        abort(400, 'The "expires" parameter was messed up. Please refer to the API for correct usage.')

    # Paste title
    paste_title = request.forms.getunicode('title')
    if paste_title.strip() == '':
        paste_title = file_name if file_name else 'Untitled'

    if fileid:  # If updating an existing file
        file_info = api_file_info(fileid)
        if file_info['user'] == 'anonymous':  # If upload is anonymous
            abort(401, 'You cannot edit anonymous uploads!')
        elif username == file_info['user']:  # If file belongs to user
                file_data = upload.file  # TODO So we can also check for 'textfield' variable

                save_path = os.path.join(DIR_FILES, fileid)

                with open(save_path, 'wb') as f:
                    f.write(file_data.read())

                filesize = os.stat(save_path).st_size

                file_info['time'] = int(time.time())
                file_info['filesize'] = int(filesize)
                file_info['title'] = paste_title
                file_info['expires'] = int(expires)
                file_info['key'] = safeurl.num_decode(fileid)
                #print(file_info)
                db.begin()
                db['files'].update(file_info, ['key'])
                db.commit()

                return 'Success! File Modified: {}'.format(fileid)
        else:  # only other possibility is that the user does not own the file!
            abort(401, """You don't have permission to edit that file!\n"""
                       """It belongs to "{}"!""".format(file_info['user']))

    key = get_random_key()
    short_key = safeurl.num_encode(key)

    # Add file to database
    db.begin()
    db['files'].insert({'user': username,
                        'expires': int(expires),  # If 0, never expires. If defined, expires @ epoch time
                        'filename': file_name,
                        'filetype': None,
                        'filesize': int(0),
                        'time': upload_time,
                        'views': 0,
                        'key': key,
                        'title': paste_title})

    save_path = os.path.join(DIR_FILES, short_key)
    with open(save_path, 'wb') as save_file:
        if upload:  # If we're using a file
            save_file.write(upload.file.read())
        else:
            save_file.write(bytes(file_text, 'utf-8'))

    # Detect Mime or get paste
    mime_type = request.forms.getunicode('mimetype')
    if not mime_type or mime_type == 'auto':
        mime_type = magic.from_file(save_path, mime=True).decode('utf-8')

    row_id = db['files'].find_one(key=key)['id']
    filesize = os.stat(save_path).st_size
    #print('Editing MIME type to {}'.format(mime_type))
    db['files'].update({'id': row_id, 'filetype': mime_type, 'filesize': filesize}, ['id'])
    db.commit()

    return 'Success! File ID: {}'.format(short_key)


#######################################
# Web UI                              #
#######################################


@ui.route('/user')
@ui.route('/user/')
def ui_user_pastes():
    """
    Page to show a user their pastes.
    """
    if not check_login_session():
        redirect('/login?redirect=/user')
        #abort(401, 'Authorization required for this page, please login.')

    username = _get_session_username()

    pastes = user_pastes(username)

    return gen_page(os.path.join(DIR_TEMPLATES, 'userpastes.tpl'),
                    {'pastes': pastes,
                     'username': username})


@ui.route('/user/changepass')
@ui.post('/user/changepass')
def user_change_password():
    """
    Change a users's password
    """
    if not check_login_session():
        redirect('/login')

    data = {'error': None,
            'success': None}

    username = _get_session_username()
    data['username'] = username

    if request.method == 'POST':
        current = request.forms.getunicode('pass-current')
        pass1 = request.forms.getunicode('pass-new')
        pass2 = request.forms.getunicode('pass-confirm')

        if not check_login(username, current, anon=False):
            data['error'] = 'Incorrect current password. Please try again.'
        elif pass1 != pass2:
            data['error'] = 'Passwords don\'t match. Please try again.'
        elif pass1 == current:
            data['error'] = 'Current and new password match'
        elif len(pass1) <= 6:
            data['error'] = 'Password too weak. Try using more than 6 characters.'
        else:
            salt = uuid4().hex
            pass_hash = sha512(bytes(salt + pass1, 'utf-8')).hexdigest()

            hash_salt_comb = '$6$' + salt + '$' + pass_hash

            db.begin()
            db['users'].update({'username': username, 'p_hash': hash_salt_comb}, ('username'))
            db.commit()
            data['success'] = True

    return gen_page(os.path.join(DIR_TEMPLATES, 'changepass.tpl'), data)


@ui.route('/delete/<fileid>')
@ui.post('/delete/<fileid>')
def ui_delete_file(fileid):
    """
    Webpage to allow users to delte files
    """
    if not check_login_session():
        redirect('/login?redirect=/delete/{}'.format(fileid))
        #abort(401, 'Authorization required for this page, please login.')

    username = _get_session_username()

    data = {'deleted': False,
            'warning': False,
            'fileid': fileid}

    file_info = api_file_info(fileid)
    data['pastetitle'] = file_info['title']

    if username != file_info['user']:
        abort(401, 'You aren\'t allowed to delete this file!')

    if request.method == 'POST':
        delete = request.forms.getunicode('delete')
        print('delete {}'.format(delete))

        if delete:
            data['deleted'] = True

            long_key = safeurl.num_decode(fileid)

            db.begin()
            db['files'].delete(key=long_key)
            db.commit()

            try:
                os.remove(os.path.join(DIR_FILES, fileid))
            except:
                abort(500, 'Something bad happened. That file isn\'t here :(')

            print('[DBG] Deleted file: {}'.format(fileid))

        else:
            redirect('/user')
    elif request.method == 'GET':
        data['warning'] = True
    else:
        pass

    return gen_page(os.path.join(DIR_TEMPLATES, 'deletefile.tpl'), data)


@ui.route('/logout')
def end_login_session():
    """
    End's a user's login session. Also removes their login cookie.
    """
    if not check_login_session():
        abort(400, 'You need to be logged in to do this!')

    # Get username of user, delete cookie, and remove session from database
    username = _get_session_username()
    remove_session(username=username)
    response.delete_cookie('login_session')

    redirect('/login')


@ui.route('/uploadcombined')  # temporary redirect for fixing links
def upload_redirect():
    redirect('/upload')


@ui.route('/upload')
def web_upload_page():
    page_data = {}
    isFile = request.query.file

    if isFile:
        page_data['isFile'] = 'true'
    else:
        page_data['isFile'] = 'false'

    username = _get_session_username()
    if username:
        page_data['username'] = username

    return gen_page(os.path.join(DIR_TEMPLATES, 'upload.tpl'), page_data)


@ui.post('/upload')  # Combined upload page
def web_upload_post():
    cookie_username = _get_session_username()
    if cookie_username:  # User is logged in w/ cookies
        if check_login_session():
            output = api_upload(web_ui=True, username=cookie_username)

            if 'Success' in output:
                key = output.split(':')[1].strip()

                data = {'key': key,
                        'username': cookie_username}

                return gen_page(os.path.join(DIR_TEMPLATES, 'uploadsuccess.tpl'), data)
    else:
        output = api_upload()

        if 'Success' in output:
            key = output.split(':')[1].strip()

            data = {'key': key}

            return gen_page(os.path.join(DIR_TEMPLATES, 'uploadsuccess.tpl'), data)


@ui.route('/register')
@ui.post('/register')  # Receiving registration data
def ui_register():
    """
    Register a user with the web frontent ui
    """
    reg_data = {'success_text': None,
                'error_text': None}

    if request.method == 'POST':  # see if we need to verify registration
        username = request.forms.getunicode('username')
        password = request.forms.getunicode('password')
        password_confirm = request.forms.getunicode('password-confirm')
        captcha_answer = request.forms.getunicode('captcha-challenge')
        captcha_id = request.cookies.getunicode('captcha_id')
        email = request.forms.getunicode('email') or None

        captcha_result = check_captcha(captcha_id, captcha_answer)

        if not sanitize(username):
            reg_data['error_text'] = """Usernames can only contain the """ \
                                     """following characters: {}""".format(ALLOWED_CHARS)
        elif len(username) < 2 or len(username) > 32:
            reg_data['error_text'] = 'Your username\'s length must be between 2 and 32 characters inclusive.'
        elif password != password_confirm:
            reg_data['error_text'] = "Those passwords don't match. Please try again."
        elif not captcha_result:
            reg_data['error_text'] = "Sorry, but you failed the captcha, please try again."
        elif len(password) <= 6:
            reg_data['error_text'] = 'Password too weak. Please try using more than 6 characters.'
        elif len(password) > 128:
            reg_data['error_text'] = """Your password is really long! Try again with a password """\
                                     """that is 128 characters or fewer."""
        else:
            # Check if username already exists
            taken = False
            if len(db['users']) > 0:
                users = db['users'].table
                query = users.select(users.c.username.ilike('%' + username + '%'))
                result = db.query(query)

                # See if there are any results
                for row in result:
                    taken = True
                    break

            if taken:
                reg_data['error_text'] = 'Sorry, but the username {} is already taken.'.format(username)
            elif username.lower().strip() == 'anonymous':
                reg_data['error_text'] = 'You are legion. You cannot register with "anonymous".'
            elif username.lower().strip() == 'root':
                reg_data['error_text'] = 'You must be a superuser to do that ;)'
            elif email and not ('@' in email):  # There are better ways to do this, but whatever
                reg_data['error_text'] = 'That email is invalid. Please try again.'
            else:
                joined = int(time.time())
                salt = uuid4().hex
                p_hash = sha512(bytes(salt + password, 'utf-8')).hexdigest()

                # TODO: Use correct hash naming scheme with 3$salt$password
                hash_salt_comb = '$' + salt + '$' + p_hash

                db.begin()
                db['users'].insert({'username': username,
                                    'joined': joined,
                                    'pastes': 0,
                                    'p_hash': hash_salt_comb,
                                    'email': email})
                db.commit()

                print('Registered user: {}'.format(username))
                reg_data['success_text'] = username

                # Log user in and stuff
                start_login_session(username)
                reg_data['username'] = username

    reg_data['captcha_data'] = captcha_provider()

    return gen_page(os.path.join(DIR_TEMPLATES, 'register.tpl'), reg_data)


def captcha_provider():
    """
    Generate random CAPTCHA using skimpyGimpy, give use CAPTCHA ID Cookie, and
    store CAPTCHA credential in database.
    """
    # Generate a random string 4 characters long
    pass_phrase = ''.join(SECRAND.choice(string.ascii_uppercase + string.digits) for _ in range(4))

    # Yes; use MD5 because the cookie doesn't have to be cryptographically secure.
    captcha_cookie_id = md5(bytes(str(uuid4()), 'utf-8')).hexdigest()

    # If there are already 10 sessions for creating cookies, expire the first session
    if len(CAPTCHA_SESSIONS) >= 10:
        _ = CAPTCHA_SESSIONS.pop(0)

    CAPTCHA_SESSIONS.append((captcha_cookie_id, pass_phrase))

    # Generate CAPTCHA with skimpyGimpy
    captcha_gen = skimpyAPI.Pre(pass_phrase, speckle=0.33, scale=1.33, color='#fff')
    captcha_test = captcha_gen.data()

    response.set_cookie('captcha_id', captcha_cookie_id)
    #print('Cookie {} coresponds to solution {}'.format(captcha_cookie_id, pass_phrase))
    return captcha_test


@ui.route('/login')
@ui.post('/login')
def ui_login():
    """
    Login for UI
    """

    login_data = {'success': False,
                  'failure': False,
                  'anon_failure': False,
                  'login_needed': False,
                  'redirect': None}

    redirect_url = request.query.redirect

    if request.method == 'POST':  # see if we need to verify registration
        username, password = get_login_creds()

        if check_login(username, password, anon=False):
            login_data['success'] = username

            if request.forms.getunicode('remember'):
                start_login_session(username, remember=True)
            else:
                start_login_session(username)

            if request.forms.getunicode('redirect'):
                redirect(request.forms.getunicode('redirect'))
        elif username.lower() == 'anonymous':
            login_data['anon_failure'] = True
        elif redirect_url:
            login_data['login_needed'] = True
            login_data['redirect'] = request.query.redirect
        else:
            login_data['failure'] = True
    else:
        if redirect_url:
            login_data['login_needed'] = True
            login_data['redirect'] = redirect_url

    return gen_page(os.path.join(DIR_TEMPLATES, 'login.tpl'), login_data)


@ui.route('/p/<paste>')
def get_ui_paste(paste):
    # Get file_data first to handle 404s
    file_data = api_file_info(paste)

    if request.method == 'GET':  # Fix for HEAD to inflate view count
        increment_view_count(paste)

    file_data['short_key'] = paste
    file_data['file_internal_src'] = os.path.join(DIR_FILES, paste)
    file_data['file_external_src'] = '/api/file/{}'.format(paste)

    if not file_data['title']:
        file_data['title'] = 'Untitled Paste'

    return gen_page(os.path.join(DIR_TEMPLATES, 'paste.tpl'), file_data)


@ui.error(404)
def error404(error_text):
    return '404 - Sorry man, but the file isn\'t here :('


def gen_page(template_path, data={}):
    """
    Generates a page with the headers and footers, along with the data dictionary given

    template_path - abosolute path of the template to use
    data - any page-specific data that needs to be given to the page
        Optional Keys:
            username - username of logged in user
    """
    data['top'] = os.path.join(DIR_TEMPLATES, 'top.html')
    data['bottom'] = os.path.join(DIR_TEMPLATES, 'bottom.html')
    data['footer'] = os.path.join(DIR_TEMPLATES, 'footer.html')
    data['stylesheets'] = tuple()
    data['scripts'] = tuple()
    data['og_image'] = '/static/favicon.ico'
    data['card_type'] = 'summary'

    if 'username' not in data:
        username = _get_session_username()
        if username:
            data['username'] = username
        else:
            data['username'] = None

    # Standard date time
    if 'time' in data:
        mod_time = data['time']

        time_diff = int(time.time()) - mod_time
        suffix = None
        time_unit = 0

        # Fast natural date implementation
        if time_diff <= 1 or time_diff <= 60:
            time_unit = time_diff  # Diff in seconds

            if time_diff == 1:
                suffix = 'second'
            else:
                suffix = 'seconds'
        elif time_diff > 60 and time_diff < 3600:
            # 60 secs/min
            time_unit = time_diff // 60

            if time_diff < 120:
                suffix = 'minute'
            else:
                suffix = 'minutes'
        elif time_diff >= 3600 and time_diff < 86400:
            # 60^2 sec/hr
            time_unit = time_diff // (60 * 60)

            if time_diff < 7200:
                suffix = 'hour'
            else:
                suffix = 'hours'
        else:
            # 60^2 * 24 sec/day
            time_unit = time_diff // (60 * 60 * 24)

            suffix = 'days'

        data['time'] = '{} {} ago'.format(time_unit, suffix)

    return template(template_path, data)


@ui.route('/')  # Index Page
@ui.route('/<filename>')  # Favicon.ico workaround
@ui.route('/static/<filename:path>')  # All other static files
def static_route(filename=None):
    """
    Route files in DIR_TEMPLATES when using --dev mode.

    Route homepage whenever using production setup.

    Also routes the index file (found in `/`)
    """
    print('Local Routing with {}'.format(filename))
    if not filename:  # Frontpage
        return gen_page(os.path.join(DIR_TEMPLATES, 'frontpage.tpl'))
    elif filename == 'favicon.ico':  # Favicon workaround
        return static_file(filename, root=DIR_TEMPLATES)
    else:
        return static_file(filename, root=DIR_TEMPLATES)


def prune_files(interval, _db):
    """
    Checks which files have expired given int(interval) as a second-format cooldown.
    """
    while True:
        time.sleep(interval)
        print('Cleaning shit up')
        now = int(time.time())
        try:
            # This is the fastest way to extract all files' information from the DB,
            # so the transaction is unlikely to get interrupted and crash!
            all_files = [fil for fil in _db['files'].all()]
        except:
            continue

        _db.begin()
        for _file in all_files:
            expires = int(_file['expires'])
            if expires <= now and expires:
                _db['files'].delete(id=_file['id'])
                _db.commit()
                key = safeurl.num_encode(_file['key'])
                try:
                    os.remove(os.path.join(DIR_FILES, key))
                except:
                    continue
                print("[DBG] Deleted file: %i\n\t%s" % (_file['id'], key))

        _db.commit()
        print('Shit cleaned up')


if __name__ == '__main__':
    # check to make sure `files` folder exists, if not, then create it.
    if not os.path.exists(DIR_FILES):
        os.makedirs(DIR_FILES)

    # Starting the prune_files(interval) thread.
    t_prune = Thread(target=prune_files, args=(CLEAN_INTERVAL, db))
    t_prune.start()

    run(ui, host=HOST, port=PORT)
